﻿/* 
 * Copyright (c) 2017, Firely (info@fire.ly) and contributors
 * See the file CONTRIBUTORS for details.
 * 
 * This file is licensed under the BSD 3-Clause license
 * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE
 */

#define DEBUG_SNAP_ELEM_ANNOTATIONS

using Hl7.Fhir.Model;
using Hl7.Fhir.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Hl7.Fhir.Specification.Snapshot
{
    /// <summary>Provides support for custom annotation types used by the <see cref="SnapshotGenerator"/>.</summary>
    public static class SnapshotGeneratorAnnotations
    {
        #region Used internally by Snapshot Generator

        internal static void RemoveSnapshotGeneratorAnnotation(this Base element)
        {
            element.RemoveConstrainedByDiffAnnotation();
            element.RemoveAppendedTextAnnotation();
        }

        /// <summary>Recursively remove any existing snapshot generator annotations from the specified snapshot element and all it's children.</summary>
        internal static void RemoveAllSnapshotGeneratorAnnotations(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.RemoveSnapshotGeneratorAnnotation();
            foreach (var child in element.EnumerateElements().SelectMany(kvp => kvp.Value as IEnumerable<Base> ?? [(Base)kvp.Value]))
            {
                child.RemoveAllSnapshotGeneratorAnnotations();
            }
        }

        /// <summary>Recursively remove any existing snapshot generator annotations from the specified snapshot elements and all their children.</summary>
        internal static void RemoveAllSnapshotGeneratorAnnotations<T>(this IEnumerable<T> elements) where T : Base
        {
            if (elements == null) { throw Error.ArgumentNull(nameof(elements)); }
            foreach (var elem in elements)
            {
                elem.RemoveAllSnapshotGeneratorAnnotations();
            }
        }

        #endregion

        #region Annotation: AppendedText

        /// <summary>
        /// Custom annotation for properties in the <see cref="StructureDefinition.SnapshotComponent"/>
        /// that have text appended with the ... notation.
        /// </summary>
        [Serializable]
        sealed class AppendedTextAnnotation
        {
            //
        }

        /// <summary>Annotate the specified snapshot element to indicate that text has been appended.</summary>
        internal static void SetAppendedTextAnnotation(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.AddAnnotation(new AppendedTextAnnotation());
        }

        /// <summary>Remove any existing appended text annotation from the specified snapshot element.</summary>
        internal static void RemoveAppendedTextAnnotation(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.RemoveAnnotations<AppendedTextAnnotation>();
        }

        public static bool HasAppendedText(this Element elem) => elem != null && elem.HasAnnotation<AppendedTextAnnotation>();

        #endregion

        #region Annotation: Created By Snapshot Generator

        /// <summary>Annotation to mark a generated element, so we can prevent duplicate re-generation.</summary>
        [Serializable]
        sealed class CreatedBySnapshotGeneratorAnnotation
        {
            public DateTimeOffset Created { get; }
            public CreatedBySnapshotGeneratorAnnotation() { Created = DateTimeOffset.UtcNow; }
        }

        /// <summary>Marks the specified element as generated by the <see cref="SnapshotGenerator"/>.</summary>
        internal static void SetCreatedBySnapshotGenerator(this Element elem) { elem?.AddAnnotation(new CreatedBySnapshotGeneratorAnnotation()); }

        /// <summary>Determines if the specified element was created by the <see cref="SnapshotGenerator"/>.</summary>
        /// <param name="elem">A FHIR <see cref="Element"/>.</param>
        /// <returns><c>true</c> if the element was created by the <see cref="SnapshotGenerator"/>, or <c>false</c> otherwise.</returns>
        public static bool IsCreatedBySnapshotGenerator(this Element elem) => elem != null && elem.HasAnnotation<CreatedBySnapshotGeneratorAnnotation>();

        #endregion

        #region Annotation: Differential Constraint

        /// <summary>
        /// Custom annotation for elements and properties in the <see cref="StructureDefinition.SnapshotComponent"/>
        /// that are constrained by the <see cref="StructureDefinition.DifferentialComponent"/>.
        /// </summary>
        [Serializable]
        sealed class ConstrainedByDiffAnnotation
        {
            //
        }

        /// <summary>Annotate the specified snapshot element to indicate that it is constrained by the differential.</summary>
        internal static void SetConstrainedByDiffAnnotation(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.AddAnnotation(new ConstrainedByDiffAnnotation());
        }

        /// <summary>Remove any existing differential constraint annotation from the specified snapshot element.</summary>
        internal static void RemoveConstrainedByDiffAnnotation(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.RemoveAnnotations<ConstrainedByDiffAnnotation>();
        }

        /// <summary>Recursively remove any existing differential constraint annotations from the specified snapshot element and all it's children.</summary>
        internal static void RemoveAllConstrainedByDiffAnnotations(this Base element)
        {
            if (element == null) { throw Error.ArgumentNull(nameof(element)); }
            element.RemoveConstrainedByDiffAnnotation();
#pragma warning disable CS0618 // Type or member is obsolete
            foreach (var child in element.Children())
#pragma warning restore CS0618 // Type or member is obsolete
            {
                child.RemoveAllConstrainedByDiffAnnotations();
            }
        }

        /// <summary>Recursively remove any existing differential constraint annotations from the specified snapshot elements and all their children.</summary>
        internal static void RemoveAllConstrainedByDiffAnnotations<T>(this IEnumerable<T> elements) where T : Base
        {
            if (elements == null) { throw Error.ArgumentNull(nameof(elements)); }
            foreach (var elem in elements)
            {
                elem.RemoveAllConstrainedByDiffAnnotations();
            }
        }

        /// <summary>
        /// Determines if the specified element is annotated as being constrained by the differential.
        /// Note that this method is non-recursive; only the specified element itself is inspected, child element annotations are ignored.
        /// Use <seealso cref="HasDiffConstraintAnnotations"/> to perform a recursive check.
        /// </summary>
        public static bool IsConstrainedByDiff(this Base elem) => elem != null && elem.HasAnnotation<ConstrainedByDiffAnnotation>();

        /// <summary>Determines if the specified element or any of it's children is annotated as being constrained by the differential.</summary>
        public static bool HasDiffConstraintAnnotations(this Base elem)
            => elem != null && (
                elem.HasAnnotation<ConstrainedByDiffAnnotation>()
#pragma warning disable CS0618 // Type or member is obsolete
                || elem.Children().Any(e => e.HasDiffConstraintAnnotations())
#pragma warning restore CS0618 // Type or member is obsolete
            );

        #endregion


        #region Annotation: Snapshot ElementDefinition

        // [WMR 20190806] Special internal annotation to save temporary reference to generated snapshot (root) element
        // Used for handling recursive profiles, e.g. Element & Extension
        // 1. Generate the new snapshot root element
        // 2. Temporarily annotate original diff root element with reference to generated snapshot root
        // 3. Generate the remaining snapshot elements
        // 4. When having to resolve a recursive reference to currently generated snapshot,
        //    retrieve the generated snapshot root element from the annotation, for merging
        //    e.g. Element.id => Element, Extension.extension => Extension
        // 5. Finally, remove temporary annotation from diff root element before returning result to caller
        //    Annotation class is internal; must explicitly clean up all instances ourselves
        //    WARNING: DeepCopy() also copies annotations, take care...

        /// <summary>For annotating a differential element definition with a reference to the associated generated snapshot element definition.</summary>
        [Serializable]
        sealed class SnapshotElementDefinitionAnnotation
        {
            /// <summary>
            /// Custom annotation type for <see cref="ElementDefinition"/> instances in the <see cref="StructureDefinition.Differential"/> component.
            /// Returns a reference to the associated <see cref="ElementDefinition"/> instance in the <see cref="StructureDefinition.Snapshot"/> component.
            /// </summary>
            public ElementDefinition SnapshotElement { get; }

            public SnapshotElementDefinitionAnnotation(ElementDefinition snapshotElement
#if DEBUG_SNAP_ELEM_ANNOTATIONS
                , ElementDefinition diffElement
#endif
                )
            {
                if (snapshotElement is null) { throw Error.ArgumentNull(nameof(snapshotElement)); }
                SnapshotElement = snapshotElement;

#if DEBUG_SNAP_ELEM_ANNOTATIONS
                if (diffElement is null) { throw Error.ArgumentNull(nameof(diffElement)); }
                DiffElement = diffElement;
#endif
            }

#if DEBUG_SNAP_ELEM_ANNOTATIONS
            // [WMR 20190806] Custom property for detecting annotations on cloned elements

            /// <summary>
            /// DEBUGGING
            /// Returns a reference to the original parent <see cref="ElementDefinition"/> that owns the annotation.
            /// Should be equal to the actual owner.
            /// Otherwise, indicates that the element has been cloned (with annotations).
            /// </summary>
            public ElementDefinition DiffElement { get; }
#endif
        }

        /// <summary>
        /// Annotate the root <see cref="ElementDefinition"/> instance in the <see cref="StructureDefinition.Differential"/> component
        /// with a reference to the associated root <see cref="ElementDefinition"/> instance in the <see cref="StructureDefinition.Snapshot"/> component.
        /// </summary>
        internal static void SetSnapshotRootElementAnnotation(this StructureDefinition sd, ElementDefinition rootElemDef)
        {
            sd?.Differential?.Element[0]?.SetSnapshotElementAnnotation(rootElemDef);
        }

        /// <summary>
        /// Annotate the specified <see cref="ElementDefinition"/> instance in the <see cref="StructureDefinition.Differential"/> component
        /// with a reference to the associated <see cref="ElementDefinition"/> instance in the <see cref="StructureDefinition.Snapshot"/> component.
        /// </summary>
        /// <param name="diffElemDef"></param>
        /// <param name="snapElemDef"></param>
        static void SetSnapshotElementAnnotation(this ElementDefinition diffElemDef, ElementDefinition snapElemDef)
        {
#if DEBUG_SNAP_ELEM_ANNOTATIONS
            Debug.WriteLine($"[{nameof(SnapshotGeneratorAnnotations)}.{nameof(SetSnapshotElementAnnotation)}] #{diffElemDef.GetHashCode()}");
#endif
            diffElemDef?.SetAnnotation(new SnapshotElementDefinitionAnnotation(snapElemDef
#if DEBUG_SNAP_ELEM_ANNOTATIONS
                , diffElemDef
#endif
                ));
        }

        /// <summary>
        /// Return the annotated reference to the associated root <see cref="ElementDefinition"/> instance
        /// in the <see cref="StructureDefinition.Snapshot"/> component, if it exists, or <c>null</c> otherwise.
        /// </summary>
        internal static ElementDefinition GetSnapshotRootElementAnnotation(this StructureDefinition sd)
            => sd?.Differential?.Element[0]?.GetSnapshotElementAnnotation();

        /// <summary>
        /// Return the annotated reference to the associated <see cref="ElementDefinition"/> instance
        /// in the <see cref="StructureDefinition.Snapshot"/> component, if it exists, or <c>null</c> otherwise.
        /// </summary>
        internal static ElementDefinition GetSnapshotElementAnnotation(this ElementDefinition ed)
#if DEBUG_SNAP_ELEM_ANNOTATIONS
        {
            var ann = ed?.Annotation<SnapshotElementDefinitionAnnotation>();

            // If the element has been cloned, the annotation no longer applies
            if (!(ann is null))
            {
                if (object.ReferenceEquals(ed, ann.DiffElement))
                {
                    return ann.SnapshotElement;
                }
                Debug.WriteLine($"[{nameof(SnapshotGeneratorAnnotations)}.{nameof(GetSnapshotElementAnnotation)}] Detected cloned annotation, removing...");
                ed?.RemoveSnapshotElementAnnotations();
            }
            return null;
        }

        /// <summary>
        /// DEBUGGING
        /// Indicates if the specified <see cref="ElementDefinition"/> is annotated
        /// with a reference to the associated snapshot element definition.
        /// </summary>
        internal static bool HasSnapshotElementAnnotation(this ElementDefinition ed)
            => !(ed?.Annotation<SnapshotElementDefinitionAnnotation>() is null);
#else
            => ed?.Annotation<SnapshotElementDefinitionAnnotation>()?.SnapshotElement;
#endif

        /// <summary>Remove all <see cref="SnapshotElementDefinitionAnnotation"/> instances from the root <see cref="ElementDefinition"/>.</summary>
        internal static void RemoveSnapshotRootElementAnnotation(this StructureDefinition sd) => sd?.Differential?.Element[0]?.RemoveSnapshotElementAnnotations();

        /// <summary>Remove all <see cref="SnapshotElementDefinitionAnnotation"/> instances from the specified <see cref="ElementDefinition"/>.</summary>
        internal static void RemoveSnapshotElementAnnotations(this ElementDefinition ed) { ed?.RemoveAnnotations<SnapshotElementDefinitionAnnotation>(); }

        #endregion
    }
}